﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml;
using Reece.Open.Mvc.Atom.Contracts;
using Reece.Open.Mvc.Atom.Factories;
using Reece.Open.Mvc.Atom.Interfaces;

namespace Reece.Open.Mvc.Atom
{
    public class AtomSerializer : IAtomSerializer
    {
        public Encoding Encoding { get; private set; }
        private XmlTextWriter xmlOut { get; set; }

        public AtomSerializer()
        {
            this.Encoding = Encoding.UTF8;
        }

        public AtomSerializer(Encoding encoding)
        {
            this.Encoding = encoding;
        }


        public void Serialize( Stream stream, object target)
        {
            MemoryStream output = new MemoryStream();
            try
            {
                xmlOut = new XmlTextWriter(output, Encoding);
                xmlOut.WriteStartDocument();
                object[] attributes = target.GetType().GetCustomAttributes(typeof(AtomRoot), true);
                if (attributes.Length == 1)
                {
                    var root = ((AtomRoot)attributes[0]);
                    xmlOut.WriteStartElement(root.ElementName, root.Namespace);
                    WriteElements(target);
                    xmlOut.WriteEndElement();
                }
                else
                {
                    throw new AtomException("There must be exactly one root.");
                }
                xmlOut.Flush();
                output.Seek(0, SeekOrigin.Begin);
                output.CopyTo(stream);
                stream.Flush();
            }
            finally
            {
                output.Dispose();
            }
        }

        private void WriteElements(object target)
        {
            var properties = target.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty);
            foreach (var property in properties)
            {
                var value = property.GetValue(target, null);
                if (value != null)
                {
                    var transform = TransformerFactory.GetTransformer(value.GetType());
                    WriteAttributes(property, value, transform);
                    WriteElement(property, value, transform);
                    WriteContent(property, value, transform);
                    WriteElementCollections(property, value);
                }
            }
        }

        private void WriteElementCollections(PropertyInfo property, object value)
        {
            AtomCollection atomCollection =
                (AtomCollection)
                property.GetCustomAttributes(typeof (AtomCollection), true).FirstOrDefault();
            if (atomCollection != null)
            {
                foreach (object item in (IEnumerable<object>) value)
                {
                    xmlOut.WriteStartElement(atomCollection.EntriesName);
                    WriteElements(item);
                    xmlOut.WriteEndElement();
                }
            }
        }

        private void WriteContent(PropertyInfo property, object value, Func<object, string> transform)
        {
            AtomContent atomContent =
                (AtomContent) property.GetCustomAttributes(typeof (AtomContent), true).FirstOrDefault();
            if (atomContent != null)
            {
                if (string.IsNullOrEmpty(atomContent.Format))
                {
                    xmlOut.WriteString(transform(value));
                }
                else
                {
                    xmlOut.WriteString(string.Format(atomContent.Format, transform(value)));
                }
            }
        }

        private void WriteElement(PropertyInfo property, object value, Func<object, string> transform)
        {
            AtomParent parent =
                (AtomParent) value.GetType().GetCustomAttributes(typeof (AtomParent), true).FirstOrDefault();
            AtomElement atomElement =
                (AtomElement) property.GetCustomAttributes(typeof (AtomElement), true).FirstOrDefault();
            if (atomElement != null)
            {
                if ( parent != null )
                {
                    xmlOut.WriteStartElement(atomElement.ElementName);
                    WriteElements(value);
                    xmlOut.WriteEndElement();
                }
                else if (string.IsNullOrEmpty(atomElement.Format))
                {
                    xmlOut.WriteElementString(atomElement.ElementName, transform(value));
                }
                else
                {
                    xmlOut.WriteElementString(atomElement.ElementName,
                                              string.Format(atomElement.Format, transform(value)));
                }
            }
        }

        private void WriteAttributes(PropertyInfo property, object value, Func<object, string> transform)
        {
            var attributes =
                property.GetCustomAttributes(typeof (AtomAttribute), true).Select(x => x as AtomAttribute);
            foreach (var attribute in attributes)
            {
                if (string.IsNullOrEmpty(attribute.Format))
                {
                    xmlOut.WriteAttributeString(attribute.AttributeName, transform(value));
                }
                else
                {
                    xmlOut.WriteAttributeString(attribute.AttributeName,
                                                string.Format(attribute.Format, transform(value)));
                }
            }
        }
    }
}
